package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"time"
)

var (
	AES_KEY = []byte("Gricks CBC Encry")
)

const (
	Size             = 16
	Offset_Uid       = 0
	Offset_Gid       = 4
	Offset_Expire    = 8
	Offset_Timestamp = 12
)

type SessionInfo struct {
	Uid       uint32
	Gid       uint32
	Expire    uint32
	Timestamp uint32
}

func (self *SessionInfo) encode() []byte {
	buffer := [Size]byte{}
	binary.LittleEndian.PutUint32(buffer[Offset_Uid:], self.Uid)
	binary.LittleEndian.PutUint32(buffer[Offset_Gid:], self.Gid)
	binary.LittleEndian.PutUint32(buffer[Offset_Expire:], self.Expire)
	binary.LittleEndian.PutUint32(buffer[Offset_Timestamp:], self.Timestamp)
	return buffer[:]
}

func (self *SessionInfo) decode(buffer []byte) {
	self.Uid = binary.LittleEndian.Uint32(buffer[Offset_Uid:])
	self.Gid = binary.LittleEndian.Uint32(buffer[Offset_Gid:])
	self.Expire = binary.LittleEndian.Uint32(buffer[Offset_Expire:])
	self.Timestamp = binary.LittleEndian.Uint32(buffer[Offset_Timestamp:])
}

func (self *SessionInfo) Encryp() ([]byte, error) {
	// CBC mode works on blocks so plaintexts may need to be padded to the
	// next whole block. For an example of such padding, see
	// https://tools.ietf.org/html/rfc5246#section-6.2.3.2. Here we'll
	// assume that the plaintext is already of the correct length.
	plaintext := self.encode()
	if len(plaintext)%aes.BlockSize != 0 {
		return nil, errors.New("invalid plaintext, not a multiple of the block size")
	}
	block, err := aes.NewCipher(AES_KEY)
	if err != nil {
		return nil, err
	}

	// The IV needs to be unique, but not secure. Therefore it's common to
	// include it at the beginning of the ciphertext.
	ciphertext := make([]byte, aes.BlockSize+len(plaintext))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, err
	}

	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)

	return ciphertext, nil
}

func (self *SessionInfo) Decryp(ciphertext []byte) error {
	block, err := aes.NewCipher(AES_KEY)
	if err != nil {
		return err
	}

	// The IV needs to be unique, but not secure. Therefore it's common to
	// include it at the beginning of the ciphertext.
	if len(ciphertext) < aes.BlockSize {
		return errors.New("invalid ciphertext, too short")
	}
	iv := ciphertext[:aes.BlockSize]
	ciphertext = ciphertext[aes.BlockSize:]

	// CBC mode always works in whole blocks.
	if len(ciphertext)%aes.BlockSize != 0 {
		return errors.New("invalid ciphertext, not a multiple of the block size")
	}

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(ciphertext, ciphertext)

	self.decode(ciphertext)
	return nil
}

func main() {
	var s SessionInfo
	s.Uid = 10001
	s.Gid = 1
	s.Timestamp = uint32(time.Now().Unix())
	s.Expire = 1 * 24 * 60 * 60
	fmt.Printf("plaintext:%d|%d|%d|%d\n", s.Uid, s.Gid, s.Expire, s.Timestamp)

	ciphertext, err := s.Encryp()
	if err != nil {
		panic(err)
	}

	fmt.Printf("ciphertext: %x\n", ciphertext)

	{
		var (
			sess SessionInfo
			text = make([]byte, len(ciphertext))
		)
		copy(text, ciphertext)
		if err := sess.Decryp(text); err != nil {
			panic(err)
		}
		fmt.Printf("plaintext:%d|%d|%d|%d\n", sess.Uid, sess.Gid, sess.Expire, sess.Timestamp)
	}

	{
		var (
			sess SessionInfo
			text = make([]byte, len(ciphertext))
		)
		copy(text, ciphertext)
		if err := sess.Decryp(text); err != nil {
			panic(err)
		}
		fmt.Printf("plaintext:%d|%d|%d|%d\n", sess.Uid, sess.Gid, sess.Expire, sess.Timestamp)
	}
}
